{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "H6oqGiIXvrMl"
      },
      "source": [
        "# CS224N: PyTorch Tutorial (Spring '24)\n",
        "\n",
        "### Author: Dilara Soylu, Ethan Chi\n",
        "\n",
        "In this notebook, we will have a basic introduction to `PyTorch` and work on a toy NLP task. Following resources have been used in preparation of this notebook:\n",
        "* [\"Word Window Classification\" tutorial notebook]((https://web.stanford.edu/class/archive/cs/cs224n/cs224n.1204/materials/ww_classifier.ipynb) by Matt Lamm, from Winter 2020 offering of CS224N\n",
        "* Official PyTorch Documentation on [Deep Learning with PyTorch: A 60 Minute Blitz](https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html) by Soumith Chintala\n",
        "* PyTorch Tutorial Notebook, [Build Basic Generative Adversarial Networks (GANs) | Coursera](https://www.coursera.org/learn/build-basic-generative-adversarial-networks-gans) by Sharon Zhou, offered on Coursera\n",
        "\n",
        "Many thanks to Angelica Sun and John Hewitt for their feedback."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "s2jvK3g_2zrK"
      },
      "source": [
        "# Please make a copy into your own Drive!"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4gk1UKaNvrMv"
      },
      "source": [
        "## Introduction\n",
        "[PyTorch](https://pytorch.org/) is a deep learning framework, one of the two main frameworks alongside [TensorFlow](https://www.tensorflow.org/). The installation can be done via Pip or Conda, as described [here](https://pytorch.org/). Let's start by importing PyTorch:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "id": "u0ukr7quvrMx"
      },
      "outputs": [],
      "source": [
        "import torch\n",
        "import torch.nn as nn\n",
        "\n",
        "# Import pprint, module we use for making our print statements prettier\n",
        "import pprint\n",
        "pp = pprint.PrettyPrinter()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "k10ZRdcBwDP3"
      },
      "source": [
        "We are all set to start our tutorial. Let's dive in!"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "OLdSN9ZXvrM0"
      },
      "source": [
        "##Part 1: Tensors\n",
        "\n",
        "**Tensors** are PyTorch's most basic building block. Each tensor is a multi-dimensional matrix; for example, a 256x256 square image might be represented by a `3x256x256` tensor, where the first dimension represents color. Here's how to create a tensor:\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "hXD7cTLTh4oF",
        "outputId": "e8662dba-7c52-4324-a4f7-fd10792fcb91"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[[1, 2, 3], [4, 5, 6]]\n"
          ]
        }
      ],
      "source": [
        "list_of_lists = [\n",
        "  [1, 2, 3],\n",
        "  [4, 5, 6],\n",
        "]\n",
        "print(list_of_lists)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "G2u7NOqWNJ7c",
        "outputId": "7be0d98b-cd5c-499d-a870-0016c0476ae8"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[1, 2, 3],\n",
            "        [4, 5, 6]])\n"
          ]
        }
      ],
      "source": [
        "quizData = torch.tensor(list_of_lists)\n",
        "print(quizData)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-VLgHhj0n3LM",
        "outputId": "361eaa7b-d240-4c79-fafd-4d3ba9991d3c"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[0, 1],\n",
            "        [2, 3],\n",
            "        [4, 5]])\n"
          ]
        }
      ],
      "source": [
        "# Initializing a tensor\n",
        "quizData = torch.tensor([\n",
        "                     [0, 1],\n",
        "                     [2, 3],\n",
        "                     [4, 5]\n",
        "                    ])\n",
        "print(quizData)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "1i7vrR1_oO4I"
      },
      "source": [
        "Each tensor has a **data type**: the major data types you'll need to worry about are floats (`torch.float32`) and integers (`torch.int`). You can specify the data type explicitly when you create the tensor:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "t7nMdqgMoLOa",
        "outputId": "684c26eb-017b-4c9c-b66f-8851568a35f8"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[0., 1.],\n",
            "        [2., 3.],\n",
            "        [4., 5.]])\n"
          ]
        }
      ],
      "source": [
        "# Initializing a tensor with an explicit data type\n",
        "# Notice the dots after the numbers, which specify that they're floats\n",
        "quizData = torch.tensor([\n",
        "                     [0, 1],\n",
        "                     [2, 3],\n",
        "                     [4, 5]\n",
        "                    ], dtype=torch.float32)\n",
        "print(quizData)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "I4vr6EYbiOEJ",
        "outputId": "344bda95-1691-47b7-f367-3d17481c2fe8"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[0.1111, 1.0000],\n",
            "        [2.0000, 3.0000],\n",
            "        [4.0000, 5.0000]])\n"
          ]
        }
      ],
      "source": [
        "# Initializing a tensor with an explicit data type\n",
        "# Notice the dots after the numbers, which specify that they're floats\n",
        "quizData = torch.tensor([\n",
        "                     [0.11111111, 1],\n",
        "                     [2, 3],\n",
        "                     [4, 5]\n",
        "                    ], dtype=torch.float32)\n",
        "print(quizData)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "jnB8J23WiUTi",
        "outputId": "fae5e40f-2ca8-4ec5-a794-4dd66e516867"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[0.1111, 1.0000],\n",
            "        [2.0000, 3.0000],\n",
            "        [4.0000, 5.0000]])\n"
          ]
        }
      ],
      "source": [
        "# Initializing a tensor with an explicit data type\n",
        "# Notice the dots after the numbers, which specify that they're floats\n",
        "quizData = torch.tensor([\n",
        "                     [0.11111111, 1],\n",
        "                     [2, 3],\n",
        "                     [4, 5]\n",
        "                    ])\n",
        "print(quizData)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "aiGCmTsrpkP-"
      },
      "source": [
        "Utility functions also exist to create tensors with given shapes and contents:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "0tn-N-z_qYj0",
        "outputId": "df692e12-9f8b-458b-b70f-8f4ee667c101"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[0., 0., 0., 0., 0.],\n",
            "        [0., 0., 0., 0., 0.]])\n"
          ]
        }
      ],
      "source": [
        "zeros = torch.zeros(2, 5)  # a tensor of all zeros\n",
        "print(zeros)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ue26M5Npqoe3",
        "outputId": "98b18695-a174-4b61-8545-a32fa1127ada"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[1., 1., 1., 1.],\n",
            "        [1., 1., 1., 1.],\n",
            "        [1., 1., 1., 1.]])\n"
          ]
        }
      ],
      "source": [
        "ones = torch.ones(3, 4)   # a tensor of all ones\n",
        "print(ones)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "hdvaujtvqokI",
        "outputId": "77123765-a189-4d8c-ac6b-e2e4a772717f"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([1, 2, 3, 4, 5, 6, 7, 8, 9])\n"
          ]
        }
      ],
      "source": [
        "rr = torch.arange(1, 10) # range from [1, 10)\n",
        "print(rr)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "lK3q3LRHipHB",
        "outputId": "23d9bdfd-5b9b-45c9-8048-fc469859d614"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([ 3,  4,  5,  6,  7,  8,  9, 10, 11])"
            ]
          },
          "execution_count": 13,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "rr + 2"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "cDQ-v6AFiyco",
        "outputId": "5ea664d0-7968-4ebe-b824-01d025b927e0"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([ 2,  4,  6,  8, 10, 12, 14, 16, 18])"
            ]
          },
          "execution_count": 14,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "rr * 2"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "OFcjwshvi3pC",
        "outputId": "81bb1488-0c44-40c4-b381-89d5ffbdf921"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "A is tensor([[1, 2],\n",
            "        [2, 3],\n",
            "        [4, 5]])\n",
            "B is tensor([[1, 2, 3, 4],\n",
            "        [5, 6, 7, 8]])\n",
            "The product is tensor([[11, 14, 17, 20],\n",
            "        [17, 22, 27, 32],\n",
            "        [29, 38, 47, 56]])\n",
            "The other product is tensor([[11, 14, 17, 20],\n",
            "        [17, 22, 27, 32],\n",
            "        [29, 38, 47, 56]])\n"
          ]
        }
      ],
      "source": [
        "a = torch.tensor([[1, 2], [2, 3], [4, 5]])      # (3, 2)\n",
        "b = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]])  # (2, 4)\n",
        "\n",
        "print(\"A is\", a)\n",
        "print(\"B is\", b)\n",
        "print(\"The product is\", a.matmul(b)) #(3, 4)\n",
        "print(\"The other product is\", a @ b) # +, -, *, @"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "SCS5z1lip9lq"
      },
      "source": [
        "The **shape** of a matrix (which can be accessed by `.shape`) is defined as the dimensions of the matrix. Here's some examples:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "fMptIpZkq0da",
        "outputId": "0466da76-e096-4814-8ae9-c3c000751db1"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "torch.Size([2, 3])\n",
            "tensor([[1, 2, 3],\n",
            "        [4, 5, 6]])\n"
          ]
        }
      ],
      "source": [
        "matr_2d = torch.tensor([[1, 2, 3], [4, 5, 6]])\n",
        "print(matr_2d.shape)\n",
        "print(matr_2d)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "aWjD7WjPqC6t",
        "outputId": "6bd07e79-fb89-462c-df24-2e5b7e52d004"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[[ 1,  2,  3,  4],\n",
            "         [-2,  5,  6,  9]],\n",
            "\n",
            "        [[ 5,  6,  7,  2],\n",
            "         [ 8,  9, 10,  4]],\n",
            "\n",
            "        [[-3,  2,  2,  1],\n",
            "         [ 4,  6,  5,  9]]])\n",
            "torch.Size([3, 2, 4])\n"
          ]
        }
      ],
      "source": [
        "matr_3d = torch.tensor([[[1, 2, 3, 4], [-2, 5, 6, 9]], [[5, 6, 7, 2], [8, 9, 10, 4]], [[-3, 2, 2, 1], [4, 6, 5, 9]]])\n",
        "print(matr_3d)\n",
        "print(matr_3d.shape)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "7wKqP85rrF-P"
      },
      "source": [
        "**Reshaping** tensors can be used to make batch operations easier (more on that later), but be careful that the data is reshaped in the order you expect:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "HmUcqHYUrMu1",
        "outputId": "ba15f448-9355-4fd4-f773-a8eb32c577cc"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The shape is currently torch.Size([15])\n",
            "The contents are currently tensor([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15])\n",
            "\n",
            "After reshaping, the shape is currently torch.Size([5, 3])\n",
            "The contents are currently tensor([[ 1,  2,  3],\n",
            "        [ 4,  5,  6],\n",
            "        [ 7,  8,  9],\n",
            "        [10, 11, 12],\n",
            "        [13, 14, 15]])\n"
          ]
        }
      ],
      "source": [
        "rr = torch.arange(1, 16)\n",
        "print(\"The shape is currently\", rr.shape)\n",
        "print(\"The contents are currently\", rr)\n",
        "print()\n",
        "rr = rr.view(5, 3)\n",
        "print(\"After reshaping, the shape is currently\", rr.shape)\n",
        "print(\"The contents are currently\", rr)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "GaykBuhoou3M"
      },
      "source": [
        "Finally, you can also inter-convert tensors with **NumPy arrays**:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ppYiPnlko1Ci",
        "outputId": "328decae-69bb-4f8d-9248-c182868bec9e"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "This is a torch.tensor tensor([[1, 0, 5]])\n",
            "This is a np.ndarray [[1 0 5]]\n"
          ]
        }
      ],
      "source": [
        "import numpy as np\n",
        "\n",
        "# numpy.ndarray --> torch.Tensor:\n",
        "arr = np.array([[1, 0, 5]])\n",
        "quizData = torch.tensor(arr)\n",
        "print(\"This is a torch.tensor\", quizData)\n",
        "\n",
        "# torch.Tensor --> numpy.ndarray:\n",
        "new_arr = quizData.numpy()\n",
        "print(\"This is a np.ndarray\", new_arr)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "hyv1l431q9yA"
      },
      "source": [
        "One of the reasons why we use **tensors** is *vectorized operations*: operations that be conducted in parallel over a particular dimension of a tensor."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Kas2MEFDsJWk",
        "outputId": "fcbd6032-cbe2-4d83-f605-a4244e69fe95"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Data is: tensor([[ 1.,  2.,  3.,  4.,  5.,  6.,  7.],\n",
            "        [ 8.,  9., 10., 11., 12., 13., 14.],\n",
            "        [15., 16., 17., 18., 19., 20., 21.],\n",
            "        [22., 23., 24., 25., 26., 27., 28.],\n",
            "        [29., 30., 31., 32., 33., 34., 35.]])\n",
            "Taking the sum over rows:\n",
            "tensor([ 28.,  77., 126., 175., 224.])\n",
            "Taking thep sum over columns:\n",
            "tensor([ 75.,  80.,  85.,  90.,  95., 100., 105.])\n",
            "Taking the stdev over rows:\n",
            "tensor([2.1602, 2.1602, 2.1602, 2.1602, 2.1602])\n"
          ]
        }
      ],
      "source": [
        "quizData = torch.arange(1, 36, dtype=torch.float32).reshape(5, 7)\n",
        "print(\"Data is:\", quizData)\n",
        "\n",
        "# We can perform operations like *sum* over each row...\n",
        "print(\"Taking the sum over rows:\")\n",
        "print(quizData.sum(dim=1)) #(5,)\n",
        "\n",
        "# or over each column.\n",
        "print(\"Taking thep sum over columns:\")\n",
        "print(quizData.sum(dim=0)) #(7,)\n",
        "\n",
        "# Other operations are available:\n",
        "print(\"Taking the stdev over rows:\")\n",
        "print(quizData.std(dim=1))\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "g1ByKJL_WYWv",
        "outputId": "d96d9b26-9742-4d25-ce39-800014ef1751"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([[[1., 2., 3.],\n",
            "         [4., 5., 6.]]])\n",
            "tensor([5., 7., 9.])\n",
            "torch.Size([3])\n"
          ]
        }
      ],
      "source": [
        "quizData = torch.arange(1, 7, dtype=torch.float32).reshape(1, 2, 3)\n",
        "print(quizData)\n",
        "print(quizData.sum(dim=0).sum(dim=0))\n",
        "print(quizData.sum(dim=0).sum(dim=0).shape)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "NPRy-xtuk2tK",
        "outputId": "90348fd4-9f07-4274-83d7-168d06288292"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor(21.)"
            ]
          },
          "execution_count": 24,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "quizData.sum()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "IJ8MjWEMxOVk"
      },
      "source": [
        "### Quiz\n",
        "\n",
        "Write code that creates a `torch.tensor` with the following contents:\n",
        "$\\begin{bmatrix} 1 & 2.2 & 9.6 \\\\ 4 & -7.2 & 6.3 \\end{bmatrix}$\n",
        "\n",
        "Now compute the average of each row (`.mean()`) and each column.\n",
        "\n",
        "What's the shape of the results?\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "BK0YInGkn3Xy"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "torch.Size([2, 3])\n",
            "tensor([[ 1.0000,  2.2000,  9.6000],\n",
            "        [ 4.0000, -7.2000,  6.3000]]) \n",
            "\n",
            "torch.Size([2])\n",
            "tensor([4.2667, 1.0333]) \n",
            "\n",
            "torch.Size([3])\n",
            "tensor([ 2.5000, -2.5000,  7.9500]) \n",
            "\n"
          ]
        }
      ],
      "source": [
        "quizData = torch.tensor([[1, 2.2, 9.6],[4, -7.2, 6.3]])\n",
        "print(quizData.shape)\n",
        "print(quizData, '\\n')\n",
        "\n",
        "row_mean = quizData.mean(dim=1)\n",
        "print(row_mean.shape)\n",
        "print(row_mean, '\\n')\n",
        "\n",
        "column_mean = quizData.mean(dim=0)\n",
        "print(column_mean.shape)\n",
        "print(column_mean, '\\n')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "V7BMktFFAkRA"
      },
      "source": [
        "**Indexing**\n",
        "\n",
        "You can access arbitrary elements of a tensor using the `[]` operator."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 30,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "fRJN7ovWDsKV",
        "outputId": "9a207766-1b7e-4489-ef5f-e5d5dbe20d15"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[[ 1.,  2.],\n",
              "         [ 3.,  4.]],\n",
              "\n",
              "        [[ 5.,  6.],\n",
              "         [ 7.,  8.]],\n",
              "\n",
              "        [[ 9., 10.],\n",
              "         [11., 12.]]])"
            ]
          },
          "execution_count": 30,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Initialize an example tensor\n",
        "x = torch.Tensor([\n",
        "                  [[1, 2], [3, 4]],\n",
        "                  [[5, 6], [7, 8]],\n",
        "                  [[9, 10], [11, 12]]\n",
        "                 ])\n",
        "x"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "M67ZiOF1Heyc",
        "outputId": "83a18ebb-cc54-4608-c96c-543fa636babc"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "torch.Size([3, 2, 2])"
            ]
          },
          "execution_count": 154,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "x.shape"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "guXKE7m8AX1K",
        "outputId": "edffdcfd-d250-4749-b31c-0908b2674529"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[1., 2.],\n",
              "        [3., 4.]])"
            ]
          },
          "execution_count": 156,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Access the 0th element, which is the first row\n",
        "x[0] # Equivalent to x[0, :]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 31,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "zn4pW2rkmXuj",
        "outputId": "514236c4-7fc5-46cb-f84f-ff3be2d4e6a2"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[ 1.,  2.],\n",
              "        [ 5.,  6.],\n",
              "        [ 9., 10.]])"
            ]
          },
          "execution_count": 31,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "x[:, 0]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "g8m8EyVvES4-"
      },
      "source": [
        "We can also index into multiple dimensions with `:`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 32,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "2Z6GFUcuEL85",
        "outputId": "13011b02-d061-43b7-ef2b-cf188323c80c"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([1., 5., 9.])"
            ]
          },
          "execution_count": 32,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Get the top left element of each element in our tensor\n",
        "x[:, 0, 0]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "TRkMhiJNnWAt",
        "outputId": "3b93bd7f-373a-47c2-a67d-71315e5e4f8e"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[[ 1.,  2.],\n",
              "         [ 3.,  4.]],\n",
              "\n",
              "        [[ 5.,  6.],\n",
              "         [ 7.,  8.]],\n",
              "\n",
              "        [[ 9., 10.],\n",
              "         [11., 12.]]])"
            ]
          },
          "execution_count": 159,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "x[:, :, :]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Rm8vc3nuXaEw"
      },
      "source": [
        "We can also access arbitrary elements in each dimension."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 34,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "R4xl6CW3RrEw",
        "outputId": "18672bf8-3d9c-4fb4-c967-7d26608cde2d"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[[1., 2.],\n",
              "         [3., 4.]],\n",
              "\n",
              "        [[1., 2.],\n",
              "         [3., 4.]],\n",
              "\n",
              "        [[5., 6.],\n",
              "         [7., 8.]],\n",
              "\n",
              "        [[5., 6.],\n",
              "         [7., 8.]]])"
            ]
          },
          "execution_count": 34,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Let's access the 0th and 1st elements, each twice\n",
        "# same as stacking x[0], x[0], x[1], x[1]\n",
        "i = torch.tensor([0, 0, 1, 1])\n",
        "x[i]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 33,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "A3QYZ8k7Wvqp",
        "outputId": "2fb10213-5441-4326-ddf3-6e5014922304"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[ 5.,  6.],\n",
              "        [ 9., 10.]])"
            ]
          },
          "execution_count": 33,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Let's access the 0th elements of the 1st and 2nd elements\n",
        "\n",
        "i = torch.tensor([1, 2])\n",
        "j = torch.tensor([0])\n",
        "x[i, j]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "WAELXC--IHS7"
      },
      "source": [
        "We can get a `Python` scalar value from a tensor with `item()`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "BM-ZujN2IGaQ",
        "outputId": "ece51f0a-f1d2-435c-d75f-0496035a7d40"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor(1.)"
            ]
          },
          "execution_count": 103,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "x[0, 0, 0]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "6NwxK7d_Ycgs",
        "outputId": "b960776c-df09-49ad-def1-e951c55c1d10"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "1.0"
            ]
          },
          "execution_count": 161,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "x[0, 0, 0].item()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bGod5kHa6OOF"
      },
      "source": [
        "### Exercise:\n",
        "\n",
        "Write code that creates a `torch.tensor` with the following contents:\n",
        "$\\begin{bmatrix} 1 & 2.2 & 9.6 \\\\ 4 & -7.2 & 6.3 \\end{bmatrix}$\n",
        "\n",
        "How do you get the first column? The first row?\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 38,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "torch.Size([2, 3])\n",
            "tensor([[ 1.0000,  2.2000,  9.6000],\n",
            "        [ 4.0000, -7.2000,  6.3000]]) \n",
            "\n",
            "tensor([1., 4.]) \n",
            "\n",
            "tensor([1.0000, 2.2000, 9.6000])\n"
          ]
        }
      ],
      "source": [
        "exerciseData = torch.tensor([[1, 2.2, 9.6],[4, -7.2, 6.3]])\n",
        "print(exerciseData.shape)\n",
        "print(exerciseData, '\\n')\n",
        "\n",
        "print(exerciseData[:, 0], '\\n')\n",
        "\n",
        "print(exerciseData[0])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Re8xiL37eAja"
      },
      "source": [
        "## Autograd\n",
        "Pytorch is well-known for its automatic differentiation feature. We can call the `backward()` method to ask `PyTorch` to calculate the gradients, which are then stored in the `grad` attribute."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 39,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "-oEvBJHWfn8H",
        "outputId": "fa65c16c-8041-4ae0-a79d-7ffbd31e2e65"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "None\n"
          ]
        }
      ],
      "source": [
        "# Create an example tensor\n",
        "# requires_grad parameter tells PyTorch to store gradients\n",
        "x = torch.tensor([2.], requires_grad=True)\n",
        "\n",
        "# Print the gradient if it is calculated\n",
        "# Currently None since x is a scalar\n",
        "pp.pprint(x.grad)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 40,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "DTJazZXkgthP",
        "outputId": "8730c813-ae43-4316-f84d-68d65ca0bd9d"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([12.])\n"
          ]
        }
      ],
      "source": [
        "# Calculating the gradient of y with respect to x\n",
        "y = x * x * 3 # 3x^2\n",
        "y.backward()\n",
        "pp.pprint(x.grad) # d(y)/d(x) = d(3x^2)/d(x) = 6x = 12"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3Hqc2oM3iV6a"
      },
      "source": [
        "Let's run backprop from a different tensor again to see what happens."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 41,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "K--Az0Xiic_z",
        "outputId": "07e848bc-f87a-476f-fc57-44a74be50874"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([24.])\n"
          ]
        }
      ],
      "source": [
        "z = x * x * 3 # 3x^2\n",
        "z.backward()\n",
        "pp.pprint(x.grad)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "4tIa4dw6Tmar",
        "outputId": "27935662-bf85-4421-9829-c43bf9d651a9"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([12.])\n"
          ]
        }
      ],
      "source": [
        "x.grad = None\n",
        "z = x * x * 3 # 3x^2\n",
        "z.backward()\n",
        "# y = x * x * 3\n",
        "pp.pprint(x.grad)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "pbyr-P4rdJ9b",
        "outputId": "7ad721e9-ef65-4309-b765-c63f9402a814"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([24.])\n"
          ]
        }
      ],
      "source": [
        "z = x * x * 3 # 3x^2\n",
        "z.backward()\n",
        "# y = x * x * 3\n",
        "pp.pprint(x.grad)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "WpvRq_wUdM5y",
        "outputId": "34aaf32c-5ac9-4c19-8dfe-c34fe8f05f6a"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([36.])\n"
          ]
        }
      ],
      "source": [
        "z = x * x * 3 # 3x^2\n",
        "z.backward()\n",
        "# y = x * x * 3\n",
        "pp.pprint(x.grad)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 46,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tensor([4.])\n"
          ]
        }
      ],
      "source": [
        "w = torch.tensor([3.], requires_grad=True)\n",
        "x = torch.tensor([2.])\n",
        "b = torch.tensor([5.], requires_grad=True)\n",
        "\n",
        "y = w*x+b\n",
        "\n",
        "z = 2*y\n",
        "\n",
        "z.backward()\n",
        "\n",
        "pp.pprint(w.grad)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "HhjPkiE6i7ja"
      },
      "source": [
        "We can see that the `x.grad` is updated to be the sum of the gradients calculated so far. When we run backprop in a neural network, we sum up all the gradients for a particular neuron before making an update. This is exactly what is happening here! This is also the reason why we need to run `zero_grad()` in every training iteration (more on this later). Otherwise our gradients would keep building up from one training iteration to the other, which would cause our updates to be wrong."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "w-9I0zZOKtXr"
      },
      "source": [
        "## Customized Backward Function\n",
        "In some rare cases, you might want to design your own operators, or calculate higher order gradients that are not supported by Pytorch. In these cases you can define your own function with customized forward & backward computation. However, keep in mind that always check if something is already implemented by Pytorch (which is very likely) before customizing your own forward & backward computation. See more at https://pytorch.org/docs/stable/notes/extending.html."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "pYLWqKIoaOyd"
      },
      "source": [
        "## Neural Network Module\n",
        "\n",
        "So far we have looked into the tensors, their properties and basic operations on tensors. These are especially useful to get familiar with if we are building the layers of our network from scratch. We will utilize these in Assignment 2, but moving forward, we will use predefined blocks in the `torch.nn` module of `PyTorch`. We will then put together these blocks to create complex networks. Let's start by importing this module with an alias so that we don't have to type `torch` every time we use it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 47,
      "metadata": {
        "id": "qUmrDpbhV4Tn"
      },
      "outputs": [],
      "source": [
        "import torch.nn as nn"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "joGvRWjEbak0"
      },
      "source": [
        "### **Linear Layer**\n",
        "We can use `nn.Linear(H_in, H_out)` to create a a linear layer. This will take a matrix of `(N, *, H_in)` dimensions and output a matrix of `(N, *, H_out)`. The `*` denotes that there could be arbitrary number of dimensions in between. The linear layer performs the operation `Ax+b`, where `A` and `b` are initialized randomly. If we don't want the linear layer to learn the bias parameters, we can initialize our layer with `bias=False`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "6XfnKI4-a5j9",
        "outputId": "ebce3d47-b720-4ff4-a279-83c2ab3513cb"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[[-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769]],\n",
              "\n",
              "        [[-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769]]], grad_fn=<ViewBackward0>)"
            ]
          },
          "execution_count": 180,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Create the inputs\n",
        "input = torch.ones(2,3,4)\n",
        "# N* H_in -> N*H_out\n",
        "\n",
        "\n",
        "# Make a linear layers transforming N,*,H_in dimensinal inputs to N,*,H_out\n",
        "# dimensional outputs\n",
        "linear = nn.Linear(4, 2)\n",
        "linear_output = linear(input)\n",
        "linear_output"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ao4XddjxeBVJ",
        "outputId": "088e2c85-511a-4508-c392-f38e38abf15f"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "torch.Size([2, 3, 10, 11, 2])"
            ]
          },
          "execution_count": 178,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "linear_output.shape"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "0_9XKtAFYpdI",
        "outputId": "992db13a-f581-4cfd-f840-fa49778e696e"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "[Parameter containing:\n",
              " tensor([[-0.1443,  0.2130,  0.2116, -0.4267],\n",
              "         [-0.3379,  0.2243,  0.3289, -0.2484]], requires_grad=True),\n",
              " Parameter containing:\n",
              " tensor([-0.4608,  0.0073], requires_grad=True)]"
            ]
          },
          "execution_count": 179,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "list(linear.parameters()) # Ax + b"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "FsyBaqzZppS8"
      },
      "outputs": [],
      "source": [
        "# Data of shape [batch_size, feature_dim] # 4\n",
        "# [batch_size, output_dim] # 2\n",
        "\n",
        "# linear layer of shape (feature_dim, output_dim)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "jAXCCu9keUlW"
      },
      "source": [
        "### **Other Module Layers**\n",
        "There are several other preconfigured layers in the `nn` module. Some commonly used examples are `nn.Conv2d`, `nn.ConvTranspose2d`, `nn.BatchNorm1d`, `nn.BatchNorm2d`, `nn.Upsample` and `nn.MaxPool2d` among many others. We will learn more about these as we progress in the course. For now, the only important thing to remember is that we can treat each of these layers as plug and play components: we will be providing the required dimensions and `PyTorch` will take care of setting them up."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yslDOK66fYWn"
      },
      "source": [
        "### **Activation Function Layer**\n",
        "We can also use the `nn` module to apply activations functions to our tensors. Activation functions are used to add non-linearity to our network. Some examples of activations functions are `nn.ReLU()`, `nn.Sigmoid()` and `nn.LeakyReLU()`. Activation functions operate on each element seperately, so the shape of the tensors we get as an output are the same as the ones we pass in."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "IrJP5CveeOON",
        "outputId": "b899dc1f-b2d8-43f5-8338-c0606271e3b8"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[[-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769]],\n",
              "\n",
              "        [[-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769],\n",
              "         [-0.8810,  0.3769]]], grad_fn=<ViewBackward0>)"
            ]
          },
          "execution_count": 181,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "linear_output"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "W9v5FjQtd4Ck",
        "outputId": "2cc3cdf3-8153-4a43-d642-420440956523"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[[0.2930, 0.5931],\n",
              "         [0.2930, 0.5931],\n",
              "         [0.2930, 0.5931]],\n",
              "\n",
              "        [[0.2930, 0.5931],\n",
              "         [0.2930, 0.5931],\n",
              "         [0.2930, 0.5931]]], grad_fn=<SigmoidBackward0>)"
            ]
          },
          "execution_count": 182,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "sigmoid = nn.Sigmoid()\n",
        "output = sigmoid(linear_output)\n",
        "output"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "RiYTthJwhEYT"
      },
      "source": [
        "### **Putting the Layers Together**\n",
        "So far we have seen that we can create layers and pass the output of one as the input of the next. Instead of creating intermediate tensors and passing them around, we can use `nn.Sequentual`, which does exactly that."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "xtJeOqLxhBLY",
        "outputId": "054d47ee-ae85-4cc7-8cd7-07e446232f84"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[[0.6822, 0.5056],\n",
              "         [0.6822, 0.5056],\n",
              "         [0.6822, 0.5056]],\n",
              "\n",
              "        [[0.6822, 0.5056],\n",
              "         [0.6822, 0.5056],\n",
              "         [0.6822, 0.5056]]], grad_fn=<SigmoidBackward0>)"
            ]
          },
          "execution_count": 183,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "block = nn.Sequential(\n",
        "    nn.Linear(4, 2),\n",
        "    nn.Sigmoid()\n",
        ")\n",
        "\n",
        "input = torch.ones(2,3,4)\n",
        "output = block(input)\n",
        "output"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "GkJ81p3GUVPM"
      },
      "source": [
        "### Custom Modules\n",
        "\n",
        "Instead of using the predefined modules, we can also build our own by extending the `nn.Module` class. For example, we can build a the `nn.Linear` (which also extends `nn.Module`) on our own using the tensor introduced earlier! We can also build new, more complex modules, such as a custom neural network. You will be practicing these in the later assignment.\n",
        "\n",
        "To create a custom module, the first thing we have to do is to extend the `nn.Module`. We can then initialize our parameters in the `__init__` function, starting with a call to the `__init__` function of the super class. All the class attributes we define which are `nn` module objects are treated as parameters, which can be learned during the training. Tensors are not parameters, but they can be turned into parameters if they are wrapped in `nn.Parameter` class.\n",
        "\n",
        "All classes extending `nn.Module` are also expected to implement a `forward(x)` function, where `x` is a tensor. This is the function that is called when a parameter is passed to our module, such as in `model(x)`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "J2P7eZiMj32_"
      },
      "outputs": [],
      "source": [
        "class MultilayerPerceptron(nn.Module):\n",
        "\n",
        "  def __init__(self, input_size, hidden_size):\n",
        "    # Call to the __init__ function of the super class\n",
        "    super(MultilayerPerceptron, self).__init__()\n",
        "\n",
        "    # Bookkeeping: Saving the initialization parameters\n",
        "    self.input_size = input_size\n",
        "    self.hidden_size = hidden_size\n",
        "\n",
        "    # Defining of our model\n",
        "    # There isn't anything specific about the naming of `self.model`. It could\n",
        "    # be something arbitrary.\n",
        "    self.model = nn.Sequential(\n",
        "        nn.Linear(self.input_size, self.hidden_size),\n",
        "        nn.ReLU(),\n",
        "        nn.Linear(self.hidden_size, self.input_size),\n",
        "        nn.Sigmoid()\n",
        "    )\n",
        "\n",
        "  def forward(self, x):\n",
        "    output = self.model(x)\n",
        "    return output"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "b2DrfLiBVjNT"
      },
      "source": [
        "Here is an alternative way to define the same class. You can see that we can replace `nn.Sequential` by defining the individual layers in the `__init__` method and connecting the in the `forward` method."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9-lqhsqwViIk"
      },
      "outputs": [],
      "source": [
        "class MultilayerPerceptron(nn.Module):\n",
        "\n",
        "  def __init__(self, input_size, hidden_size):\n",
        "    # Call to the __init__ function of the super class\n",
        "    super(MultilayerPerceptron, self).__init__()\n",
        "\n",
        "    # Bookkeeping: Saving the initialization parameters\n",
        "    self.input_size = input_size\n",
        "    self.hidden_size = hidden_size\n",
        "\n",
        "    # Defining of our layers\n",
        "    self.linear = nn.Linear(self.input_size, self.hidden_size)\n",
        "    self.relu = nn.ReLU()\n",
        "    self.linear2 = nn.Linear(self.hidden_size, self.input_size)\n",
        "    self.sigmoid = nn.Sigmoid()\n",
        "\n",
        "  def forward(self, x):\n",
        "    linear = self.linear(x)\n",
        "    relu = self.relu(linear)\n",
        "    linear2 = self.linear2(relu)\n",
        "    output = self.sigmoid(linear2)\n",
        "    return output"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "YQelcFo5bXgU"
      },
      "source": [
        "Now that we have defined our class, we can instantiate it and see what it does."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "cXi0T0FZbV0y",
        "outputId": "7add78d5-712f-41e2-fd65-d9149331c020"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[0.4621, 0.5310, 0.4358, 0.6027, 0.6664],\n",
              "        [0.4217, 0.5628, 0.3208, 0.5732, 0.6326]], grad_fn=<SigmoidBackward0>)"
            ]
          },
          "execution_count": 187,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Make a sample input\n",
        "input = torch.randn(2, 5)\n",
        "\n",
        "# Create our model\n",
        "model = MultilayerPerceptron(5, 3)\n",
        "\n",
        "# Pass our input through our model\n",
        "model(input)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "tCCbjc-Fb2-B"
      },
      "source": [
        "We can inspect the parameters of our model with `named_parameters()` and `parameters()` methods."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "7d23soYIb2WZ",
        "outputId": "b520a8fc-3911-495c-cd48-49f1ade47beb"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "[('linear.weight',\n",
              "  Parameter containing:\n",
              "  tensor([[-0.0359,  0.0409, -0.2182,  0.0993,  0.0216],\n",
              "          [ 0.3572, -0.0213,  0.0229, -0.1651,  0.3042],\n",
              "          [ 0.1404,  0.1992, -0.2556, -0.1605,  0.0395]], requires_grad=True)),\n",
              " ('linear.bias',\n",
              "  Parameter containing:\n",
              "  tensor([ 0.1759, -0.1290,  0.0760], requires_grad=True)),\n",
              " ('linear2.weight',\n",
              "  Parameter containing:\n",
              "  tensor([[ 0.2302,  0.0376, -0.4018],\n",
              "          [-0.4662, -0.4792,  0.1319],\n",
              "          [-0.5094,  0.3908, -0.1104],\n",
              "          [ 0.3688,  0.1750, -0.3135],\n",
              "          [ 0.3631,  0.3253, -0.2241]], requires_grad=True)),\n",
              " ('linear2.bias',\n",
              "  Parameter containing:\n",
              "  tensor([-0.1755,  0.3826, -0.4531,  0.3189,  0.5156], requires_grad=True))]"
            ]
          },
          "execution_count": 188,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "list(model.named_parameters())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "x5JegycOdMFy"
      },
      "source": [
        "## Optimization\n",
        "We have showed how gradients are calculated with the `backward()` function. Having the gradients isn't enought for our models to learn. We also need to know how to update the parameters of our models. This is where the optomozers comes in. `torch.optim` module contains several optimizers that we can use. Some popular examples are `optim.SGD` and `optim.Adam`. When initializing optimizers, we pass our model parameters, which can be accessed with `model.parameters()`, telling the optimizers which values it will be optimizing. Optimizers also has a learning rate (`lr`) parameter, which determines how big of an update will be made in every step. Different optimizers have different hyperparameters as well."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "W0F-TvV0kk-I"
      },
      "outputs": [],
      "source": [
        "import torch.optim as optim"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "wgak6o5dlQWF"
      },
      "source": [
        "After we have our optimization function, we can define a `loss` that we want to optimize for. We can either define the loss ourselves, or use one of the predefined loss function in `PyTorch`, such as `nn.BCELoss()`. Let's put everything together now! We will start by creating some dummy data."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "dGYFiaT_vXBn",
        "outputId": "4f4a1f82-f708-4f8b-c5c2-5232cbbe8615"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[ 1.4604,  2.1893,  0.8026,  0.6062,  0.9302],\n",
              "        [ 0.2317,  1.1250,  1.1276,  0.3870,  2.0563],\n",
              "        [ 0.3060,  2.9154,  0.0979,  0.8160,  1.9484],\n",
              "        [ 0.2887,  0.1449,  0.1708, -0.0307,  2.0472],\n",
              "        [ 0.6013,  1.8131,  0.7032,  0.2523, -0.0232],\n",
              "        [ 2.6874,  0.5569,  0.4187,  1.2088,  0.2832],\n",
              "        [ 2.8989,  1.5482,  0.3915,  0.5046,  0.1376],\n",
              "        [ 1.1544,  0.3065, -0.2656,  2.5511,  0.2247],\n",
              "        [ 1.7423, -0.1496,  2.0271,  1.0739,  1.1934],\n",
              "        [ 0.3968,  0.2009,  0.6164,  1.8774,  1.1588]])"
            ]
          },
          "execution_count": 190,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Create the y data\n",
        "y = torch.ones(10, 5)\n",
        "\n",
        "# Add some noise to our goal y to generate our x\n",
        "# We want out model to predict our original data, albeit the noise\n",
        "x = y + torch.randn_like(y)\n",
        "x"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "BEsiOdpWvfLj"
      },
      "source": [
        "Now, we can define our model, optimizer and the loss function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "2oA2XsdsbN8p",
        "outputId": "0c1235ec-9df4-4d58-ce75-8c5d123ac40f"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "0.18392038345336914"
            ]
          },
          "execution_count": 191,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Instantiate the model\n",
        "model = MultilayerPerceptron(5, 3)\n",
        "\n",
        "# Define the optimizer\n",
        "adam = optim.Adam(model.parameters(), lr=1e-1)\n",
        "\n",
        "# Define loss using a predefined loss function\n",
        "loss_function = nn.MSELoss()\n",
        "\n",
        "# Calculate how our model is doing now\n",
        "y_pred = model(x)\n",
        "loss_function(y_pred, y).item()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "pXmyqSwmWfNu"
      },
      "outputs": [],
      "source": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "gtxU7Y8ZufSR"
      },
      "source": [
        "Let's see if we can have our model achieve a smaller loss. Now that we have everything we need, we can setup our training loop."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ogl6-Ctmuek6",
        "outputId": "b860ab08-f6a0-4f04-f9d0-19f4a8df595b"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Epoch 0: traing loss: 0.18392038345336914\n",
            "Epoch 1: traing loss: 0.1214178055524826\n",
            "Epoch 2: traing loss: 0.0665205642580986\n",
            "Epoch 3: traing loss: 0.031827643513679504\n",
            "Epoch 4: traing loss: 0.012266852892935276\n",
            "Epoch 5: traing loss: 0.003626907477155328\n",
            "Epoch 6: traing loss: 0.000923578510992229\n",
            "Epoch 7: traing loss: 0.00023057256476022303\n",
            "Epoch 8: traing loss: 6.007583942846395e-05\n",
            "Epoch 9: traing loss: 1.6709067494957708e-05\n"
          ]
        }
      ],
      "source": [
        "# Set the number of epoch, which determines the number of training iterations\n",
        "n_epoch = 10\n",
        "\n",
        "for epoch in range(n_epoch):\n",
        "  # Set the gradients to 0\n",
        "  adam.zero_grad()\n",
        "\n",
        "  # Get the model predictions\n",
        "  y_pred = model(x)\n",
        "\n",
        "  # Get the loss\n",
        "  loss = loss_function(y_pred, y)\n",
        "\n",
        "  # Print stats\n",
        "  print(f\"Epoch {epoch}: traing loss: {loss}\")\n",
        "\n",
        "  # Compute the gradients\n",
        "  loss.backward()\n",
        "\n",
        "  # Take a step to optimize the weights\n",
        "  adam.step()\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "ZrMJ8AmqeCY-",
        "outputId": "3e70c28c-5f01-45ef-fd27-c2fcf428698d"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "[Parameter containing:\n",
              " tensor([[-0.3774, -0.2663, -0.2254, -0.3110, -0.0272],\n",
              "         [ 1.1547,  0.4672,  0.5807,  0.3853,  0.7197],\n",
              "         [ 0.5740,  1.1962,  0.7437,  0.9969,  1.0613]], requires_grad=True),\n",
              " Parameter containing:\n",
              " tensor([0.0332, 0.8876, 0.4750], requires_grad=True),\n",
              " Parameter containing:\n",
              " tensor([[ 0.3789,  0.8695,  0.9322],\n",
              "         [-0.0926,  0.9584,  0.9451],\n",
              "         [ 0.1741,  0.5681,  0.5995],\n",
              "         [-0.5748,  1.1098,  0.6488],\n",
              "         [-0.0048,  1.2007,  1.2029]], requires_grad=True),\n",
              " Parameter containing:\n",
              " tensor([0.8713, 1.1602, 0.9818, 0.8432, 0.4917], requires_grad=True)]"
            ]
          },
          "execution_count": 193,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "list(model.parameters())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4nXApd82wlsF"
      },
      "source": [
        "You can see that our loss is decreasing. Let's check the predictions of our model now and see if they are close to our original `y`, which was all `1s`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "gRqE7P9EtvuS",
        "outputId": "2b2db865-2bb2-4598-85ee-625b9fab7ef9"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[1.0000, 1.0000, 0.9994, 1.0000, 1.0000],\n",
              "        [0.9999, 1.0000, 0.9984, 0.9998, 1.0000],\n",
              "        [1.0000, 1.0000, 0.9996, 1.0000, 1.0000],\n",
              "        [0.9980, 0.9989, 0.9884, 0.9975, 0.9995],\n",
              "        [0.9990, 0.9994, 0.9925, 0.9985, 0.9998],\n",
              "        [0.9999, 1.0000, 0.9987, 0.9999, 1.0000],\n",
              "        [1.0000, 1.0000, 0.9991, 1.0000, 1.0000],\n",
              "        [0.9995, 0.9997, 0.9952, 0.9993, 0.9999],\n",
              "        [1.0000, 1.0000, 0.9991, 1.0000, 1.0000],\n",
              "        [0.9997, 0.9998, 0.9963, 0.9994, 1.0000]], grad_fn=<SigmoidBackward0>)"
            ]
          },
          "execution_count": 194,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# See how our model performs on the training data\n",
        "y_pred = model(x)\n",
        "y_pred"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "IJng31_Pi2R6",
        "outputId": "92a56e62-3e8b-4a45-b6d0-b7581bb6ce6b"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "tensor([[1.0000, 1.0000, 0.9999, 1.0000, 1.0000],\n",
              "        [0.9973, 0.9985, 0.9859, 0.9976, 0.9993],\n",
              "        [1.0000, 1.0000, 0.9997, 1.0000, 1.0000],\n",
              "        [0.9997, 0.9998, 0.9966, 0.9991, 1.0000],\n",
              "        [1.0000, 1.0000, 0.9999, 1.0000, 1.0000],\n",
              "        [0.9983, 0.9990, 0.9893, 0.9971, 0.9996],\n",
              "        [1.0000, 1.0000, 0.9993, 1.0000, 1.0000],\n",
              "        [1.0000, 1.0000, 0.9994, 1.0000, 1.0000],\n",
              "        [0.9999, 1.0000, 0.9988, 0.9999, 1.0000],\n",
              "        [0.9999, 0.9999, 0.9979, 0.9998, 1.0000]], grad_fn=<SigmoidBackward0>)"
            ]
          },
          "execution_count": 195,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Create test data and check how our model performs on it\n",
        "x2 = y + torch.randn_like(y)\n",
        "y_pred = model(x2)\n",
        "y_pred"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "8WNk6oIZw2xo"
      },
      "source": [
        "Great! Looks like our model almost perfectly learned to filter out the noise from the `x` that we passed in!"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "P8rUNk_1xG1v"
      },
      "source": [
        "## Demo: Word Window Classification\n",
        "\n",
        "Until this part of the notebook, we have learned the fundamentals of PyTorch and built a basic network solving a toy task. Now we will attempt to solve an example NLP task. Here are the things we will learn:\n",
        "\n",
        "1. Data: Creating a Dataset of Batched Tensors\n",
        "2. Modeling\n",
        "3. Training\n",
        "4. Prediction\n",
        "\n",
        "In this section, our goal will be to train a model that will find the words in a sentence corresponding to a `LOCATION`, which will be always of span `1` (meaning that `San Fransisco` won't be recognized as a `LOCATION`). Our task is called `Word Window Classification` for a reason. Instead of letting our model to only take a look at one word in each forward pass, we would like it to be able to consider the context of the word in question. That is, for each word, we want our model to be aware of the surrounding words. Let's dive in!"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_amzuUx8BJXI"
      },
      "source": [
        "### Data\n",
        "\n",
        "The very first task of any machine learning project is to set up our training set. Usually, there will be a training corpus we will be utilizing. In NLP tasks, the corpus would generally be a `.txt` or `.csv` file where each row corresponds to a sentence or a tabular datapoint. In our toy task, we will assume that we have already read our data and the corresponding labels into a `Python` list."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "mDiI1PLMw10z"
      },
      "outputs": [],
      "source": [
        "# Our raw data, which consists of sentences\n",
        "corpus = [\n",
        "          \"We always come to Paris\",\n",
        "          \"The professor is from Australia\",\n",
        "          \"I live in Stanford\",\n",
        "          \"He comes from Taiwan\",\n",
        "          \"The capital of Turkey is Ankara\"\n",
        "         ]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "t33Uke9AE22s"
      },
      "source": [
        "#### Preprocessing\n",
        "\n",
        "To make it easier for our models to learn, we usually apply a few preprocessing steps to our data. This is especially important when dealing with text data. Here are some examples of text preprocessing:\n",
        "* **Tokenization**: Tokenizing the sentences into words.\n",
        "* **Lowercasing**: Changing all the letters to be lowercase.\n",
        "* **Noise removal:** Removing special characters (such as punctuations).\n",
        "* **Stop words removal**: Removing commonly used words.\n",
        "\n",
        "Which preprocessing steps are necessary is determined by the task at hand. For example, although it is useful to remove special characters in some tasks, for others they may be important (for example, if we are dealing with multiple languages). For our task, we will lowercase our words and tokenize.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "fTGn8ANTzZXT"
      },
      "outputs": [],
      "source": [
        "# The preprocessing function we will use to generate our training examples\n",
        "# Our function is a simple one, we lowercase the letters\n",
        "# and then tokenize the words.\n",
        "def preprocess_sentence(sentence):\n",
        "  return sentence.lower().split()\n",
        "\n",
        "# Create our training set\n",
        "train_sentences = [preprocess_sentence(sent) for sent in corpus]\n",
        "train_sentences"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "k4jzo5tp0Hza"
      },
      "source": [
        "For each training example we have, we should also have a corresponding label. Recall that the goal of our model was to determine which words correspond to a `LOCATION`. That is, we want our model to output `0` for all the words that are not `LOCATION`s and `1` for the ones that are `LOCATION`s."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "3wo1kcMAHFw7"
      },
      "outputs": [],
      "source": [
        "# Set of locations that appear in our corpus\n",
        "locations = set([\"australia\", \"ankara\", \"paris\", \"stanford\", \"taiwan\", \"turkey\"])\n",
        "\n",
        "# Our train labels\n",
        "train_labels = [[1 if word in locations else 0 for word in sent] for sent in train_sentences]\n",
        "train_labels"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "qgVKH9M3RtPx"
      },
      "source": [
        "#### Converting Words to Embeddings\n",
        "\n",
        "Let's look at our training data a little more closely. Each datapoint we have is a sequence of words. On the other hand, we know that machine learning models work with numbers in vectors. How are we going to turn words into numbers? You may be thinking embeddings and you are right!\n",
        "\n",
        "Imagine that we have an embedding lookup table `E`, where each row corresponds to an embedding. That is, each word in our vocabulary would have a corresponding embedding row `i` in this table. Whenever we want to find an embedding for a word, we will follow these steps:\n",
        "1. Find the corresponding index `i` of the word in the embedding table: `word->index`.\n",
        "2. Index into the embedding table and get the embedding: `index->embedding`.\n",
        "\n",
        "Let's look at the first step. We should assign all the words in our vocabulary to a corresponding index. We can do it as follows:\n",
        "1. Find all the unique words in our corpus.\n",
        "2. Assign an index to each."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "SjTDlfPyVp5z"
      },
      "outputs": [],
      "source": [
        "# Find all the unique words in our corpus\n",
        "vocabulary = set(w for s in train_sentences for w in s)\n",
        "vocabulary"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "IOxnKznOWXSC"
      },
      "source": [
        "`vocabulary` now contains all the words in our corpus. On the other hand, during the test time, we can see words that are not contained in our vocabulary. If we can figure out a way to represent the unknown words, our model can still reason about whether they are a `LOCATION` or not, since we are also looking at the neighboring words for each prediction.\n",
        "\n",
        "We introduce a special token, `<unk>`, to tackle the words that are out of vocabulary. We could pick another string for our unknown token if we wanted. The only requirement here is that our token should be unique: we should only be using this token for unknown words. We will also add this special token to our vocabulary."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ygxYuE1DYeR3"
      },
      "outputs": [],
      "source": [
        "# Add the unknown token to our vocabulary\n",
        "vocabulary.add(\"<unk>\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Bsf4haL94AFu"
      },
      "source": [
        "Earlier we mentioned that our task was called `Word Window Classification` because our model is looking at the surroundings words in addition to the given word when it needs to make a prediction.\n",
        "\n",
        "For example, let's take the sentence \"We always come to Paris\". The corresponding training label for this sentence is `0, 0, 0, 0, 1` since only Paris, the last word, is a `LOCATION`. In one pass (meaning a call to `forward()`), our model will try to generate the correct label for one word. Let's say our model is trying to generate the correct label `1` for `Paris`. If we only allow our model to see `Paris`, but nothing else, we will miss out on the important information that the word `to` often times appears with `LOCATION`s.\n",
        "\n",
        "Word windows allow our model to consider the surrounding `+N` or `-N` words of each word when making a prediction. In our earlier example for `Paris`, if we have a window size of 1, that means our model will look at the words that come immediately before and after `Paris`, which are `to`, and, well, nothing. Now, this raises another issue. `Paris` is at the end of our sentence, so there isn't another word following it. Remember that we define the input dimensions of our `PyTorch` models when we are initializing them. If we set the window size to be `1`, it means that our model will be accepting `3` words in every pass. We cannot have our model expect `2` words from time to time.\n",
        "\n",
        "The solution is to introduce a special token, such as `<pad>`, that will be added to our sentences to make sure that every word has a valid window around them. Similar to `<unk>` token, we could pick another string for our pad token if we wanted, as long as we make sure it is used for a unique purpose."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ZVQsjYi6ZegI"
      },
      "outputs": [],
      "source": [
        "# Add the <pad> token to our vocabulary\n",
        "vocabulary.add(\"<pad>\")\n",
        "\n",
        "# Function that pads the given sentence\n",
        "# We are introducing this function here as an example\n",
        "# We will be utilizing it later in the tutorial\n",
        "def pad_window(sentence, window_size, pad_token=\"<pad>\"):\n",
        "  window = [pad_token] * window_size\n",
        "  return window + sentence + window\n",
        "\n",
        "# Show padding example\n",
        "window_size = 2\n",
        "pad_window(train_sentences[0], window_size=window_size)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cqvgWKwSNpAd"
      },
      "source": [
        "Now that our vocabularly is ready, let's assign an index to each of our words."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "BNCTQnKDa4oh"
      },
      "outputs": [],
      "source": [
        "# We are just converting our vocabularly to a list to be able to index into it\n",
        "# Sorting is not necessary, we sort to show an ordered word_to_ind dictionary\n",
        "# That being said, we will see that having the index for the padding token\n",
        "# be 0 is convenient as some PyTorch functions use it as a default value\n",
        "# such as nn.utils.rnn.pad_sequence, which we will cover in a bit\n",
        "ix_to_word = sorted(list(vocabulary))\n",
        "\n",
        "# Creating a dictionary to find the index of a given word\n",
        "word_to_ix = {word: ind for ind, word in enumerate(ix_to_word)}\n",
        "word_to_ix"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "pt-0SK67hMVo"
      },
      "outputs": [],
      "source": [
        "ix_to_word[1]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ELZuteqbdWd1"
      },
      "source": [
        "Great! We are ready to convert our training sentences into a sequence of indices corresponding to each token."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ZNOxip15bMfH"
      },
      "outputs": [],
      "source": [
        "# Given a sentence of tokens, return the corresponding indices\n",
        "def convert_token_to_indices(sentence, word_to_ix):\n",
        "  indices = []\n",
        "  for token in sentence:\n",
        "    # Check if the token is in our vocabularly. If it is, get it's index.\n",
        "    # If not, get the index for the unknown token.\n",
        "    if token in word_to_ix:\n",
        "      index = word_to_ix[token]\n",
        "    else:\n",
        "      index = word_to_ix[\"<unk>\"]\n",
        "    indices.append(index)\n",
        "  return indices\n",
        "\n",
        "# More compact version of the same function\n",
        "def _convert_token_to_indices(sentence, word_to_ix):\n",
        "  return [word_to_ind.get(token, word_to_ix[\"<unk>\"]) for token in sentence]\n",
        "\n",
        "# Show an example\n",
        "example_sentence = [\"we\", \"always\", \"come\", \"to\", \"kuwait\"]\n",
        "example_indices = convert_token_to_indices(example_sentence, word_to_ix)\n",
        "restored_example = [ix_to_word[ind] for ind in example_indices]\n",
        "\n",
        "print(f\"Original sentence is: {example_sentence}\")\n",
        "print(f\"Going from words to indices: {example_indices}\")\n",
        "print(f\"Going from indices to words: {restored_example}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_jsXw8cB1xpH"
      },
      "source": [
        "In the example above, `kuwait` shows up as `<unk>`, because it is not included in our vocabulary. Let's convert our `train_sentences` to `example_padded_indices`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "JRaKQwSJH-1d"
      },
      "outputs": [],
      "source": [
        "# Converting our sentences to indices\n",
        "example_padded_indices = [convert_token_to_indices(s, word_to_ix) for s in train_sentences]\n",
        "example_padded_indices"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "JZULjHBjHsEK"
      },
      "source": [
        "Now that we have an index for each word in our vocabularly, we can create an embedding table with `nn.Embedding` class in `PyTorch`. It is called as follows `nn.Embedding(num_words, embedding_dimension)` where `num_words` is the number of words in our vocabulary and the `embedding_dimension` is the dimension of the embeddings we want to have. There is nothing fancy about `nn.Embedding`: it is just a wrapper class around a trainabe `NxE` dimensional tensor, where `N` is the number of words in our vocabulary and `E` is the number of embedding dimensions. This table is initially random, but it will change over time. As we train our network, the gradients will be backpropagated all the way to the embedding layer, and hence our word embeddings would be updated. We will initiliaze the embedding layer we will use for our model in our model, but we are showing an example here."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "F4AgHzv91VXx"
      },
      "outputs": [],
      "source": [
        "# Creating an embedding table for our words\n",
        "embedding_dim = 5\n",
        "embeds = nn.Embedding(len(vocabulary), embedding_dim)\n",
        "\n",
        "# Printing the parameters in our embedding table\n",
        "list(embeds.parameters())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "wI7ZTt4OkpPp"
      },
      "source": [
        "To get the word embedding for a word in our vocabulary, all we need to do is to create a lookup tensor. The lookup tensor is just a tensor containing the index we want to look up `nn.Embedding` class expects an index tensor that is of type Long Tensor, so we should create our tensor accordingly."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "nkldmcepjfh_"
      },
      "outputs": [],
      "source": [
        "# Get the embedding for the word Paris\n",
        "index = word_to_ix[\"paris\"]\n",
        "index_tensor = torch.tensor(index, dtype=torch.long)\n",
        "paris_embed = embeds(index_tensor)\n",
        "paris_embed"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "mUsdwBOxm6B4"
      },
      "outputs": [],
      "source": [
        "# We can also get multiple embeddings at once\n",
        "index_paris = word_to_ix[\"paris\"]\n",
        "index_ankara = word_to_ix[\"ankara\"]\n",
        "indices = [index_paris, index_ankara]\n",
        "indices_tensor = torch.tensor(indices, dtype=torch.long)\n",
        "embeddings = embeds(indices_tensor)\n",
        "embeddings"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2bgSW3LPltkF"
      },
      "source": [
        "Usually, we define the embedding layer as part of our model, which you will see in the later sections of our notebook."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DHCXeQOamHU1"
      },
      "source": [
        "#### Batching Sentences\n",
        "\n",
        "We have learned about batches in class. Waiting our whole training corpus to be processed before making an update is constly. On the other hand, updating the parameters after every training example causes the loss to be less stable between updates. To combat these issues, we instead update our parameters after training on a batch of data. This allows us to get a better estimate of the gradient of the global loss. In this section, we will learn how to structure our data into batches using the `torch.util.data.DataLoader` class.\n",
        "\n",
        "We will be calling the `DataLoader` class as follows: `DataLoader(data, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)`.  The `batch_size` parameter determines the number of examples per batch. In every epoch, we will be iterating over all the batches using the `DataLoader`. The order of batches is deterministic by default, but we can ask `DataLoader` to shuffle the batches by setting the `shuffle` parameter to `True`. This way we ensure that we don't encounter a bad batch multiple times.\n",
        "\n",
        "If provided, `DataLoader` passes the batches it prepares to the `collate_fn`. We can write a custom function to pass to the `collate_fn` parameter in order to print stats about our batch or perform extra processing. In our case, we will use the `collate_fn` to:\n",
        "1. Window pad our train sentences.\n",
        "2. Convert the words in the training examples to indices.\n",
        "3. Pad the training examples so that all the sentences and labels have the same length. Similarly, we also need to pad the labels. This creates an issue because when calculating the loss, we need to know the actual number of words in a given example. We will also keep track of this number in the function we pass to the `collate_fn` parameter.\n",
        "\n",
        "Because our version of the `collate_fn` function will need to access to our `word_to_ix` dictionary (so that it can turn words into indices), we will make use of the `partial` function in `Python`, which passes the parameters we give to the function we pass it."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "OkvvVlo4jgFm"
      },
      "outputs": [],
      "source": [
        "from torch.utils.data import DataLoader\n",
        "from functools import partial\n",
        "\n",
        "def custom_collate_fn(batch, window_size, word_to_ix):\n",
        "  # Break our batch into the training examples (x) and labels (y)\n",
        "  # We are turning our x and y into tensors because nn.utils.rnn.pad_sequence\n",
        "  # method expects tensors. This is also useful since our model will be\n",
        "  # expecting tensor inputs.\n",
        "  x, y = zip(*batch)\n",
        "\n",
        "  # Now we need to window pad our training examples. We have already defined a\n",
        "  # function to handle window padding. We are including it here again so that\n",
        "  # everything is in one place.\n",
        "  def pad_window(sentence, window_size, pad_token=\"<pad>\"):\n",
        "    window = [pad_token] * window_size\n",
        "    return window + sentence + window\n",
        "\n",
        "  # Pad the train examples.\n",
        "  x = [pad_window(s, window_size=window_size) for s in x]\n",
        "\n",
        "  # Now we need to turn words in our training examples to indices. We are\n",
        "  # copying the function defined earlier for the same reason as above.\n",
        "  def convert_tokens_to_indices(sentence, word_to_ix):\n",
        "    return [word_to_ix.get(token, word_to_ix[\"<unk>\"]) for token in sentence]\n",
        "\n",
        "  # Convert the train examples into indices.\n",
        "  x = [convert_tokens_to_indices(s, word_to_ix) for s in x]\n",
        "\n",
        "  # We will now pad the examples so that the lengths of all the example in\n",
        "  # one batch are the same, making it possible to do matrix operations.\n",
        "  # We set the batch_first parameter to True so that the returned matrix has\n",
        "  # the batch as the first dimension.\n",
        "  pad_token_ix = word_to_ix[\"<pad>\"]\n",
        "\n",
        "  # pad_sequence function expects the input to be a tensor, so we turn x into one\n",
        "  x = [torch.LongTensor(x_i) for x_i in x]\n",
        "  x_padded = nn.utils.rnn.pad_sequence(x, batch_first=True, padding_value=pad_token_ix)\n",
        "\n",
        "  # We will also pad the labels. Before doing so, we will record the number\n",
        "  # of labels so that we know how many words existed in each example.\n",
        "  lengths = [len(label) for label in y]\n",
        "  lenghts = torch.LongTensor(lengths)\n",
        "\n",
        "  y = [torch.LongTensor(y_i) for y_i in y]\n",
        "  y_padded = nn.utils.rnn.pad_sequence(y, batch_first=True, padding_value=0)\n",
        "\n",
        "  # We are now ready to return our variables. The order we return our variables\n",
        "  # here will match the order we read them in our training loop.\n",
        "  return x_padded, y_padded, lenghts"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "1Q0gXea-bCsz"
      },
      "source": [
        "This function seems long, but it really doesn't have to be. Check out the alternative version below where we remove the extra function declarations and comments."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "dZfcmAJXbLcq"
      },
      "outputs": [],
      "source": [
        "def _custom_collate_fn(batch, window_size, word_to_ix):\n",
        "  # Prepare the datapoints\n",
        "  x, y = zip(*batch)\n",
        "  x = [pad_window(s, window_size=window_size) for s in x]\n",
        "  x = [convert_tokens_to_indices(s, word_to_ix) for s in x]\n",
        "\n",
        "  # Pad x so that all the examples in the batch have the same size\n",
        "  pad_token_ix = word_to_ix[\"<pad>\"]\n",
        "  x = [torch.LongTensor(x_i) for x_i in x]\n",
        "  x_padded = nn.utils.rnn.pad_sequence(x, batch_first=True, padding_value=pad_token_ix)\n",
        "\n",
        "  # Pad y and record the length\n",
        "  lengths = [len(label) for label in y]\n",
        "  lenghts = torch.LongTensor(lengths)\n",
        "  y = [torch.LongTensor(y_i) for y_i in y]\n",
        "  y_padded = nn.utils.rnn.pad_sequence(y, batch_first=True, padding_value=0)\n",
        "\n",
        "  return x_padded, y_padded, lenghts"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "dS1WuQO0Khxx"
      },
      "source": [
        "Now, we can see the `DataLoader` in action."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "RfB0JKL2vZ6p"
      },
      "outputs": [],
      "source": [
        "# Parameters to be passed to the DataLoader\n",
        "quizData = list(zip(train_sentences, train_labels))\n",
        "batch_size = 2\n",
        "shuffle = True\n",
        "window_size = 2\n",
        "collate_fn = partial(custom_collate_fn, window_size=window_size, word_to_ix=word_to_ix)\n",
        "\n",
        "# Instantiate the DataLoader\n",
        "loader = DataLoader(quizData, batch_size=batch_size, shuffle=shuffle, collate_fn=collate_fn)\n",
        "\n",
        "# Go through one loop\n",
        "counter = 0\n",
        "for batched_x, batched_y, batched_lengths in loader:\n",
        "  print(f\"Iteration {counter}\")\n",
        "  print(\"Batched Input:\")\n",
        "  print(batched_x)\n",
        "  print(\"Batched Labels:\")\n",
        "  print(batched_y)\n",
        "  print(\"Batched Lengths:\")\n",
        "  print(batched_lengths)\n",
        "  print(\"\")\n",
        "  counter += 1"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "93QOZSsMTFNF"
      },
      "source": [
        "The batched input tensors you see above will be passed into our model. On the other hand, we started off saying that our model will be a window classifier. The way our input tensors are currently formatted, we have all the words in a sentence in one datapoint. When we pass this input to our model, it needs to create the windows for each word, make a prediction as to whether the center word is a `LOCATION` or not for each window, put the predictions together and return.\n",
        "\n",
        "We could avoid this problem if we formatted our data by breaking it into windows beforehand. In this example, we will instead how our model take care of the formatting.\n",
        "\n",
        "Given that our `window_size` is `N` we want our model to make a prediction on every `2N+1` tokens. That is, if we have an input with `9` tokens, and a `window_size` of `2`, we want our model to return `5` predictions. This makes sense because before we padded it with `2` tokens on each side, our input also had `5` tokens in it!\n",
        "\n",
        "We can create these windows by using for loops, but there is a faster `PyTorch` alternative, which is the `unfold(dimension, size, step)` method. We can create the windows we need using this method as follows:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "RMZu-pxLVxHQ"
      },
      "outputs": [],
      "source": [
        "# Print the original tensor\n",
        "print(f\"Original Tensor: \")\n",
        "print(batched_x)\n",
        "print(\"\")\n",
        "\n",
        "# Create the 2 * 2 + 1 chunks\n",
        "chunk = batched_x.unfold(1, window_size*2 + 1, 1)\n",
        "print(f\"Windows: \")\n",
        "print(chunk)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "XlDbOpeoSKxd"
      },
      "source": [
        "### Model\n",
        "\n",
        "Now that we have prepared our data, we are ready to build our model. We have learned how to write custom `nn.Module` classes. We will do the same here and put everything we have learned so far together."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "JLTU4h76NLYm"
      },
      "outputs": [],
      "source": [
        "class WordWindowClassifier(nn.Module):\n",
        "\n",
        "  def __init__(self, hyperparameters, vocab_size, pad_ix=0):\n",
        "    super(WordWindowClassifier, self).__init__()\n",
        "\n",
        "    \"\"\" Instance variables \"\"\"\n",
        "    self.window_size = hyperparameters[\"window_size\"]\n",
        "    self.embed_dim = hyperparameters[\"embed_dim\"]\n",
        "    self.hidden_dim = hyperparameters[\"hidden_dim\"]\n",
        "    self.freeze_embeddings = hyperparameters[\"freeze_embeddings\"]\n",
        "\n",
        "    \"\"\" Embedding Layer\n",
        "    Takes in a tensor containing embedding indices, and returns the\n",
        "    corresponding embeddings. The output is of dim\n",
        "    (number_of_indices * embedding_dim).\n",
        "\n",
        "    If freeze_embeddings is True, set the embedding layer parameters to be\n",
        "    non-trainable. This is useful if we only want the parameters other than the\n",
        "    embeddings parameters to change.\n",
        "\n",
        "    \"\"\"\n",
        "    self.embeds = nn.Embedding(vocab_size, self.embed_dim, padding_idx=pad_ix)\n",
        "    if self.freeze_embeddings:\n",
        "      self.embed_layer.weight.requires_grad = False\n",
        "\n",
        "    \"\"\" Hidden Layer\n",
        "    \"\"\"\n",
        "    full_window_size = 2 * window_size + 1\n",
        "    self.hidden_layer = nn.Sequential(\n",
        "      nn.Linear(full_window_size * self.embed_dim, self.hidden_dim),\n",
        "      nn.Tanh()\n",
        "    )\n",
        "\n",
        "    \"\"\" Output Layer\n",
        "    \"\"\"\n",
        "    self.output_layer = nn.Linear(self.hidden_dim, 1)\n",
        "\n",
        "    \"\"\" Probabilities\n",
        "    \"\"\"\n",
        "    self.probabilities = nn.Sigmoid()\n",
        "\n",
        "  def forward(self, inputs):\n",
        "    \"\"\"\n",
        "    Let B:= batch_size\n",
        "        L:= window-padded sentence length\n",
        "        D:= self.embed_dim\n",
        "        S:= self.window_size\n",
        "        H:= self.hidden_dim\n",
        "\n",
        "    inputs: a (B, L) tensor of token indices\n",
        "    \"\"\"\n",
        "    B, L = inputs.size()\n",
        "\n",
        "    \"\"\"\n",
        "    Reshaping.\n",
        "    Takes in a (B, L) LongTensor\n",
        "    Outputs a (B, L~, S) LongTensor\n",
        "    \"\"\"\n",
        "    # Fist, get our word windows for each word in our input.\n",
        "    token_windows = inputs.unfold(1, 2 * self.window_size + 1, 1)\n",
        "    _, adjusted_length, _ = token_windows.size()\n",
        "\n",
        "    # Good idea to do internal tensor-size sanity checks, at the least in comments!\n",
        "    assert token_windows.size() == (B, adjusted_length, 2 * self.window_size + 1)\n",
        "\n",
        "    \"\"\"\n",
        "    Embedding.\n",
        "    Takes in a torch.LongTensor of size (B, L~, S)\n",
        "    Outputs a (B, L~, S, D) FloatTensor.\n",
        "    \"\"\"\n",
        "    embedded_windows = self.embeds(token_windows)\n",
        "\n",
        "    \"\"\"\n",
        "    Reshaping.\n",
        "    Takes in a (B, L~, S, D) FloatTensor.\n",
        "    Resizes it into a (B, L~, S*D) FloatTensor.\n",
        "    -1 argument \"infers\" what the last dimension should be based on leftover axes.\n",
        "    \"\"\"\n",
        "    embedded_windows = embedded_windows.view(B, adjusted_length, -1)\n",
        "\n",
        "    \"\"\"\n",
        "    Layer 1.\n",
        "    Takes in a (B, L~, S*D) FloatTensor.\n",
        "    Resizes it into a (B, L~, H) FloatTensor\n",
        "    \"\"\"\n",
        "    layer_1 = self.hidden_layer(embedded_windows)\n",
        "\n",
        "    \"\"\"\n",
        "    Layer 2\n",
        "    Takes in a (B, L~, H) FloatTensor.\n",
        "    Resizes it into a (B, L~, 1) FloatTensor.\n",
        "    \"\"\"\n",
        "    output = self.output_layer(layer_1)\n",
        "\n",
        "    \"\"\"\n",
        "    Softmax.\n",
        "    Takes in a (B, L~, 1) FloatTensor of unnormalized class scores.\n",
        "    Outputs a (B, L~, 1) FloatTensor of (log-)normalized class scores.\n",
        "    \"\"\"\n",
        "    output = self.probabilities(output)\n",
        "    output = output.view(B, -1)\n",
        "\n",
        "    return output"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Avy1fnyAvEcd"
      },
      "source": [
        "### Training\n",
        "\n",
        "We are now ready to put everything together. Let's start with preparing our data and intializing our model. We can then intialize our optimizer and define our loss function. This time, instead of using one of the predefined loss function as we did before, we will define our own loss function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "bInu1VqjHsfj"
      },
      "outputs": [],
      "source": [
        "# Prepare the data\n",
        "quizData = list(zip(train_sentences, train_labels))\n",
        "batch_size = 2\n",
        "shuffle = True\n",
        "window_size = 2\n",
        "collate_fn = partial(custom_collate_fn, window_size=window_size, word_to_ix=word_to_ix)\n",
        "\n",
        "# Instantiate a DataLoader\n",
        "loader = DataLoader(quizData, batch_size=batch_size, shuffle=shuffle, collate_fn=collate_fn)\n",
        "\n",
        "# Initialize a model\n",
        "# It is useful to put all the model hyperparameters in a dictionary\n",
        "model_hyperparameters = {\n",
        "    \"batch_size\": 4,\n",
        "    \"window_size\": 2,\n",
        "    \"embed_dim\": 25,\n",
        "    \"hidden_dim\": 25,\n",
        "    \"freeze_embeddings\": False,\n",
        "}\n",
        "\n",
        "vocab_size = len(word_to_ix)\n",
        "model = WordWindowClassifier(model_hyperparameters, vocab_size)\n",
        "\n",
        "# Define an optimizer\n",
        "learning_rate = 0.01\n",
        "optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)\n",
        "\n",
        "# Define a loss function, which computes to binary cross entropy loss\n",
        "def loss_function(batch_outputs, batch_labels, batch_lengths):\n",
        "    # Calculate the loss for the whole batch\n",
        "    bceloss = nn.BCELoss()\n",
        "    loss = bceloss(batch_outputs, batch_labels.float())\n",
        "\n",
        "    # Rescale the loss. Remember that we have used lengths to store the\n",
        "    # number of words in each training example\n",
        "    loss = loss / batch_lengths.sum().float()\n",
        "\n",
        "    return loss"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "pHxpxDkFHfQE"
      },
      "source": [
        "Unlike our earlier example, this time instead of passing all of our training data to the model at once in each epoch, we will be utilizing batches. Hence, in each training epoch iteration, we also iterate over the batches."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "QL9IDgIOvHca"
      },
      "outputs": [],
      "source": [
        "# Function that will be called in every epoch\n",
        "def train_epoch(loss_function, optimizer, model, loader):\n",
        "\n",
        "  # Keep track of the total loss for the batch\n",
        "  total_loss = 0\n",
        "  for batch_inputs, batch_labels, batch_lengths in loader:\n",
        "    # Clear the gradients\n",
        "    optimizer.zero_grad()\n",
        "    # Run a forward pass\n",
        "    outputs = model.forward(batch_inputs)\n",
        "    # Compute the batch loss\n",
        "    loss = loss_function(outputs, batch_labels, batch_lengths)\n",
        "    # Calculate the gradients\n",
        "    loss.backward()\n",
        "    # Update the parameteres\n",
        "    optimizer.step()\n",
        "    total_loss += loss.item()\n",
        "\n",
        "  return total_loss\n",
        "\n",
        "\n",
        "# Function containing our main training loop\n",
        "def train(loss_function, optimizer, model, loader, num_epochs=10000):\n",
        "\n",
        "  # Iterate through each epoch and call our train_epoch function\n",
        "  for epoch in range(num_epochs):\n",
        "    epoch_loss = train_epoch(loss_function, optimizer, model, loader)\n",
        "    if epoch % 100 == 0: print(epoch_loss)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cjf75cnzJ4n6"
      },
      "source": [
        "Let's start training!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Kav8kwVBJ6XW"
      },
      "outputs": [],
      "source": [
        "num_epochs = 1000\n",
        "train(loss_function, optimizer, model, loader, num_epochs=num_epochs)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "T-k7Pav4LdQJ"
      },
      "source": [
        "### Prediction\n",
        "\n",
        "Let's see how well our model is at making predictions. We can start by creating our test data."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "-v5X69a2Lkbm"
      },
      "outputs": [],
      "source": [
        "# Create test sentences\n",
        "test_corpus = [\"She comes from Paris\"]\n",
        "test_sentences = [s.lower().split() for s in test_corpus]\n",
        "test_labels = [[0, 0, 0, 1]]\n",
        "\n",
        "# Create a test loader\n",
        "test_data = list(zip(test_sentences, test_labels))\n",
        "batch_size = 1\n",
        "shuffle = False\n",
        "window_size = 2\n",
        "collate_fn = partial(custom_collate_fn, window_size=2, word_to_ix=word_to_ix)\n",
        "test_loader = torch.utils.data.DataLoader(test_data,\n",
        "                                           batch_size=1,\n",
        "                                           shuffle=False,\n",
        "                                           collate_fn=collate_fn)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "HlBa8xaNMZgv"
      },
      "source": [
        "Let's loop over our test examples to see how well we are doing."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "dGYn8CAoMTjX"
      },
      "outputs": [],
      "source": [
        "for test_instance, labels, _ in test_loader:\n",
        "  outputs = model.forward(test_instance)\n",
        "  print(labels)\n",
        "  print(outputs)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "iebrU4ZibVIR"
      },
      "outputs": [],
      "source": []
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "cs336",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.12.10"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
